package com.obixlabs.commons.types;

import static com.obixlabs.commons.util.SimpleHashCodeCalculator.calculateFromFields;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.obixlabs.commons.ObixRuntimeException;
import com.obixlabs.commons.util.SimpleHashCodeCalculator;

/**
 * <p>
 * Utility class for operations common to Java objects.
 * </p>
 */
public class ObjectTypeUtilities
{
	/**
	 * <p>
	 * Private constructor to prevent instantiation of utility class.
	 * </p>
	 */	
	private ObjectTypeUtilities(){}
	
	/**
	 * <p>
	 * Performs a deep clone of the given object.
	 * </p>
	 * 
	 * @param <T>	The type of the object to clone.
	 * @param object	The object to clone.
	 * @return	A deep clone of the given object.
	 */
	@SuppressWarnings("unchecked")
	public static<T> T cloneObject(T object)
	{
		T result;
		
		try
		{
			result = (T) object.getClass().newInstance();			
			Field[] declaredFields = object.getClass().getDeclaredFields();
			
			boolean accessibility;
			Field field;
			Object fieldValue;
			int modifiers;
			for (int i=0;i<declaredFields.length;i++)
			{
				field = declaredFields[i];
				modifiers = field.getModifiers();
				if (!Modifier.isStatic(modifiers) && !Modifier.isFinal(modifiers))
				{								
					accessibility = field.isAccessible();				
					field.setAccessible(true);
					fieldValue=field.get(object);
					field.set(result, fieldValue);
					field.setAccessible(accessibility);
				}
			}			
		}
		catch (Exception exce)
		{ throw new ObixRuntimeException("Could not clone object due to Exception.", exce); }
		
		return result;		
	}
	
	/**
	 * <p>
	 * Calculates the hashcode of the given object from its declared fields.
	 * This method {@link #getDeclaredFieldValues(Object) extracts the object's attributes}, 
	 * and invokes the {@link SimpleHashCodeCalculator#calculateFromFields(Object...)
	 * obix commons hashcode calculator} with these.
	 * </p>
	 * 
	 * @return A unique numerical hashcode calculated from the object's 
	 * {@link #getDeclaredFieldValues(Object) attributes}.
	 */
	public static int hashCode(Object obj)
	{ return calculateFromFields(getDeclaredFieldValues(obj)); }
	
	/**
	 * <p>
	 * Evaluates if the specified objects are equal to each other.
	 * </p>
	 * 
	 * @param lhs	The left-hand-side of the comparison operation.
	 * @param rhs	The right-hand-side of the comparison operation.
	 * @return	If the specified objects are equal to each other.
	 */
	public static boolean equals(Object lhs, Object rhs)
	{
		boolean result;
		
		if (lhs == null || rhs == null)
			result = false;
		else if (lhs == rhs)
			result = true;
		else if (lhs.getClass().isInstance(rhs))
		{
			Object[] lhsFields = getDeclaredFieldValues(lhs).toArray();
			Object[] rhsFields = getDeclaredFieldValues(rhs).toArray();			
			result = Arrays.equals(lhsFields, rhsFields);
		}
		else result = false;
		
		return result;
	}

	/**
	 * <p>
	 * Extracts the attributes of the specified object. The object's 
	 * attributes are given by the fields declared in the object's class. 
	 * </p>
	 * 
	 * @param obj	The object whose attributes are to be extracted.
	 * @return	An list containing the object's attributes.
	 */
	public static List<Object> getDeclaredFieldValues(Object obj)
	{
		List<Object> result = new ArrayList<Object>();
		
		try
		{
			Field[] declaredFields = obj.getClass().getDeclaredFields();

			boolean accessibility;
			for (Field field : declaredFields)
			{			
				accessibility = field.isAccessible();				
				field.setAccessible(true);
				result.add(field.get(obj));
				field.setAccessible(accessibility);
			}
		}
		catch (Exception exce) { throw new ObixRuntimeException(exce); }
		
		return result;
	}
	
	
	/**
	 * <p>
	 * Returns the declared fields in the given class with the specified annotation.
	 * </p>
	 * 
	 * @param clazz	The class whose annotated fields are to be extracted.
	 * @param annotation	The  annotation to search for.
	 * @return	A list of all the fields in the class with the given annotation.
	 */
	public static List<Field> getAnnotatedFields(Class<?> clazz, Class<? extends Annotation> annotation)
	{
		List<Field> result = new ArrayList<Field>();
		try
		{
			Field[] declaredFields = clazz.getDeclaredFields();
			Field field;
			for (int i=0;i<declaredFields.length;i++)
			{
				field = declaredFields[i];
				if (field.isAnnotationPresent(annotation))
					result.add(field);
			}			
		}
		catch (Exception exce) { throw new ObixRuntimeException(exce); }		
		return result;
	}
	
}